﻿/*
 * Copyright (c) <2012> <guangboo49@gmail.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this 
 * software and associated documentation files (the "Software"), to deal in the Software 
 * without restriction, including without limitation the rights to use, copy, modify, merge, 
 * publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 
 * to whom the Software is furnished to do so, subject to the following conditions: 
 * The above copyright notice and this permission notice shall be included in all copies or 
 * substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;

namespace Optparse {
    public class OptionParser {
        private IDictionary<string, IArgOption> _aliasArgOptions;
        private IDictionary<string, IArgOption> _nameArgOptions;
        
        private OptionGroup _unkownGroup;

        public OptionParser(string usage, string version) {
            _aliasArgOptions = new Dictionary<string, IArgOption>();
            _nameArgOptions = new Dictionary<string, IArgOption>();
            _unkownGroup = new OptionGroup("Unkown");

            Groups = new List<OptionGroup>();
            Groups.Add(_unkownGroup);

            this.Usage = usage;
            this.Version = version;
        }

        public IList<OptionGroup> Groups { get; private set; }
        public string Usage { get; set; }
        public string Version { get; set; }

        public void AddOption(IArgOption option) {
            if (option == null)
                throw new ArgumentNullException("option", "The option must not be null.");

            _unkownGroup.AddOption(option);
            _aliasArgOptions.Add(option.Alias, option);
            _nameArgOptions.Add(option.Name, option);
        }

        public void AddOptionGroup(OptionGroup group) {
            this.Groups.Add(group);
            foreach (IArgOption opt in group) {
                _aliasArgOptions.Add(opt.Alias, opt);
                _nameArgOptions.Add(opt.Name, opt);
            }
        }

        public void PrintUsage() {
            int width = Console.BufferWidth - 2;
            string usage = this.Usage;
            usage = usage.Replace("%product", AssemblyProduct)
                .Replace("%version", Version)
                .Replace("%title", AssemblyTitle)
                .Replace("%copyright", AssemblyCopyright)
                .Replace("%description", AssemblyDescription)
                .Replace("%company", AssemblyCompany);

            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("{0}\r\n", usage);
            //sb.Append(this.Usage);
            Console.WriteLine(sb.ToString());

            Console.WriteLine("\n{0}", ("-- OPTIONS --").Justify(width));

            foreach (OptionGroup group in this.Groups) {
                if (group != this._unkownGroup) {
                    Console.WriteLine("\n{0}", ("-- " + group.Title + " --").Justify(width));
                    if (!string.IsNullOrEmpty(group.Description)) {
                        Console.WriteLine("{0}\n", group.Description.Justify(width));
                    }
                }
                foreach (IArgOption opt in group) {
                    Console.WriteLine(" {0, -3} {1,-10} {2,-10} {3}", opt.Alias, opt.Name, opt.Description, opt.HelpText.PadLeft(width - 27, 27));
                }
            }
        }

        public void Parse(string cmdline) {
            CommandParser parser = new CommandParser(cmdline);

            CommandParser.Token progToken = parser.NextToken();
            CommandParser.Token lastToken = progToken;
            CommandParser.Token token;
            IArgOption opt = null, lastOpt = null;

            while (!parser.EOF) {
                token = parser.NextToken();

                if (token.Type == CommandParser.TokenType.ALIAS) {
                    if (lastOpt != null && (lastToken.Type == CommandParser.TokenType.ALIAS || lastToken.Type == CommandParser.TokenType.NAME)) {
                        lastOpt.Parse(string.Empty);
                    }
                    if (_aliasArgOptions.TryGetValue(token.Symbol, out opt)) {
                        lastOpt = opt;
                        lastToken = token;
                        continue;
                    }
                } else if (token.Type == CommandParser.TokenType.NAME) {
                    if (lastOpt != null && (lastToken.Type == CommandParser.TokenType.ALIAS || lastToken.Type == CommandParser.TokenType.NAME)) {
                        lastOpt.Parse(string.Empty);
                    }

                    if (_nameArgOptions.TryGetValue(token.Symbol, out opt)) {
                        lastOpt = opt;
                        lastToken = token;
                        continue;
                    }
                } else if (token.Type == CommandParser.TokenType.VALUE) {
                    if (lastOpt == null) {
                        //throw new ArgValueParseException(null, token.Symbol, "The option value must match a option name.");
                    } else {
                        if (lastToken.Type == CommandParser.TokenType.ALIAS || lastToken.Type == CommandParser.TokenType.NAME) {
                            lastOpt.Parse(token.Symbol);
                        } else {
                            throw new ArgValueParseException(null, token.Symbol, "The option value must match a option name.");
                        }
                    }

                    lastToken = token;
                    lastOpt = null;
                }
            }

            if (lastOpt != null && (lastToken.Type == CommandParser.TokenType.ALIAS || lastToken.Type == CommandParser.TokenType.NAME)) {
                lastOpt.Parse(string.Empty);
            }
        }

        public void Parse() {
            Parse(Environment.CommandLine);
        }

        public object GetValue(string aliasOrName) {
            if (string.IsNullOrEmpty(aliasOrName))
                throw new ArgumentNullException("aliasOrName", "The parameter aliasOrName must not be null or empty.");

            IArgOption opt;
            if (FormatUtil.ValidateAlias(aliasOrName)) {
                if (_aliasArgOptions.TryGetValue(aliasOrName, out opt))
                    return opt.Value;
                throw new ArgumentException(string.Format("The alias {0} option is not found.", aliasOrName), "aliasOrName");
            } else if (FormatUtil.ValidateName(aliasOrName)) {
                if (_nameArgOptions.TryGetValue(aliasOrName, out opt))
                    return opt.Value;
                throw new ArgumentException(string.Format("The name {0} option is not found.", aliasOrName), "aliasOrName");
            } else {
                throw new ArgumentException("The parameter is not required style. the style -f or --file is expected.", "aliasOrName");
            }
        }

        #region assembly info

        private string AssemblyTitle {
            get {
                object[] attributes = Assembly.GetEntryAssembly().GetCustomAttributes(typeof(AssemblyTitleAttribute), false);
                if (attributes.Length == 0) {
                    return "";
                }
                return ((AssemblyTitleAttribute)attributes[0]).Title;
            }
        }

        private string AssemblyVersion {
            get {
                return Assembly.GetEntryAssembly().GetName().Version.ToString();
            }
        }

        private string AssemblyDescription {
            get {
                object[] attributes = Assembly.GetEntryAssembly().GetCustomAttributes(typeof(AssemblyDescriptionAttribute), false);
                if (attributes.Length == 0) {
                    return "";
                }
                return ((AssemblyDescriptionAttribute)attributes[0]).Description;
            }
        }

        private string AssemblyProduct {
            get {
                object[] attributes = Assembly.GetEntryAssembly().GetCustomAttributes(typeof(AssemblyProductAttribute), false);
                if (attributes.Length == 0) {
                    return "";
                }
                return ((AssemblyProductAttribute)attributes[0]).Product;
            }
        }

        private string AssemblyCopyright {
            get {
                object[] attributes = Assembly.GetEntryAssembly().GetCustomAttributes(typeof(AssemblyCopyrightAttribute), false);
                if (attributes.Length == 0) {
                    return "";
                }
                return ((AssemblyCopyrightAttribute)attributes[0]).Copyright;
            }
        }

        private string AssemblyCompany {
            get {
                object[] attributes = Assembly.GetEntryAssembly().GetCustomAttributes(typeof(AssemblyCompanyAttribute), false);
                if (attributes.Length == 0) {
                    return "";
                }
                return ((AssemblyCompanyAttribute)attributes[0]).Company;
            }
        }

        #endregion

        private class CommandParser {
            private string raw;
            private int index;
            private int length;
            private readonly char SINGLE_QUOTE = '\'';
            private readonly char QUOTE = '"';

            public CommandParser(string cmdline) {
                raw = cmdline;
                index = 0;
                length = cmdline.Length;
            }

            public bool EOF {
                get { return index >= length; }
            }

            public Token NextToken() {
                SkipWhiteSpace();

                if (EOF)
                    return new Token(string.Empty, TokenType.VALUE);

                char ch,quote,last;
                ch = quote = raw[index++];

                if (quote == SINGLE_QUOTE || quote == QUOTE) {
                    if (EOF)
                        throw new CommandLineParserException(index - 1);

                    ch = raw[index++];
                    if (ch == quote) {
                        return new Token(string.Empty, TokenType.VALUE);
                    }

                    if (EOF)
                        throw new CommandLineParserException(index - 1);

                    TinyCharBuffer buffer = new TinyCharBuffer(64);
                    buffer.Append(ch);

                    last = ch;
                    ch = raw[index++];

                    do {
                        buffer.Append(ch);
                        if (EOF)
                            break;

                        last = ch;
                        ch = raw[index++];
                    } while (ch != quote || last == '\\');

                    if (ch != quote || last == '\\') {
                        throw new CommandLineParserException(index - 1);
                    }

                    return new Token(buffer.ToString(), TokenType.VALUE);
                } else {
                    TinyCharBuffer buffer = new TinyCharBuffer(64);
                    do {
                        buffer.Append(ch);
                        if(EOF)
                            break;
                        ch = raw[index++];
                    } while (!char.IsWhiteSpace(ch));

                    string str = buffer.ToString();
                    if (FormatUtil.ValidateAlias(str)) {
                        return new Token(str, TokenType.ALIAS);
                    } else if (FormatUtil.ValidateName(str)) {
                        return new Token(str, TokenType.NAME);
                    } else {
                        return new Token(str, TokenType.VALUE);
                    }
                }
            }

            private void SkipWhiteSpace() {
                while (index < length && char.IsWhiteSpace(raw[index])) {
                    index += 1;
                }
            }

            #region nested token class

            internal class Token {
                public string Symbol;
                public TokenType Type;

                public Token(string symb, TokenType type) {
                    this.Symbol = symb;
                    this.Type = type;
                }
            }

            internal enum TokenType {
                VALUE,
                ALIAS,
                NAME
            }

            #endregion

            #region nested tinycharbuffer class

            private class TinyCharBuffer {
                private char[] buffer;
                private int index;
                private int size;
                private int capacity;
                public const char EOF = '\0';

                public TinyCharBuffer(int capacity) {
                    buffer = new char[capacity + 1];
                    index = 0;
                    this.capacity = size = capacity;
                }

                public void Append(char ch) {
                    if (index >= size) {
                        char[] newbuf = new char[size + capacity + 1];
                        Array.Copy(buffer, 0, newbuf, 0, index);
                        buffer = newbuf;
                        size += capacity;
                    }

                    buffer[index++] = ch;
                }

                public char Pop() {
                    if (index > 0) {
                        index -= 1;
                        return buffer[index];
                    }

                    return EOF;
                }

                public override string ToString() {
                    return new string(buffer, 0, index);
                }
            }

            #endregion
        }
    }
}
